AWS ParallelCluster スポットインスタンス中断時ローカルストレージのデータを S3 に退避する方法
クラウド HPC において、スポットインスタンスの利用は非常にコスト効率の高い選択肢です。しかし、長時間のジョブ実行中にスポットインスタンスの中断が発生すると、計算中のデータを失うリスクがあります。本記事では、AWS ParallelCluster でスポットインスタンスを使用する際に、インスタンスストア(ローカルストレージ)のデータを S3 に退避する方法を紹介します。
本ブログで紹介するアーキテクチャは以下です。
導入背景
AWS ParallelCluster では、コスト効率を高めるためにコンピュートノードでスポットインスタンスが活用されています。私のユースケースでは、インスタンスストア(ローカルストレージ)を中間ファイルの保存先として利用しています。インスタンスストアは高速な I/O 性能と一時的なストレージ領域を提供しますが、スポットインスタンスが中断された場合にデータを失うリスクがあります。
特に長期間実行されるジョブの場合、スポット中断によるデータロストは時間と、コスト面で手痛い出戻りとなります。そこで、スポット中断の通知を受けてから、インスタンスストアのデータを S3 に退避させる仕組みを構築することでこの課題を解決します。
短期間で終わるジョブの場合は再度ジョブを投げることとし、ここではその問題は扱いません。
アーキテクチャ概要
本構成では、インスタンスストア付きの EC2 インスタンス(インスタンスタイプ名に'd'が付くもの)のデフォルトマウントポイント /scratch
配下のデータを退避対象とします。
- EventBridge がスポットインスタンスの中断通知を検知
- Lambda が起動し、Systems Manager Run Command を実行
- 対象 EC2 インスタンスの
/scratch
領域のデータを S3 へコピー
青枠の箇所は CDK のサンプルコードを提供します。検証に使用したクラスターのコンフィグルも本ブログに掲載してあります。
注意事項
対応必須
コンピュートノードの IAM ロールに、退避用 S3 バケットへのアクセス権限の追加が必須です。ParallelCluster の設定で対応してください。
その他留意点
- スポット中断の通知から実際の中断までの時間は 2 分しかありません。
- 大容量データの場合は全てを退避できない可能性があります
- データ転送料と、データ転送速度の観点から、S3 の VPC エンドポイント(ゲートウェイ型)の利用を推奨します
実装の詳細
データ退避用の S3 バケット作成
S3 バケット名は任意の名前を設定できます。このサンプルではテスト目的でスタック削除時に S3 バケットも削除する設定になっていますが、実運用時はデータ保持のため RETAIN
を推奨します。
const backupBucket = new s3.Bucket(this, 'RescueSpotDataBucket', {
bucketName: 'rescue-spot-data-bucket',
removalPolicy: cdk.RemovalPolicy.DESTROY, # 実運用時は RETAIN 推奨
autoDeleteObjects: true,
encryption: s3.BucketEncryption.S3_MANAGED,
});
EventBridge でスポット中断検知
以下のブログで紹介されている内容です。
データ退避コマンド実行 Lambda
データソースを /scratch
としています。コンピュートノードの EBS にデータを保存している場合は、適宜パスを修正してください。
- 中断対象のインスタンス ID は EventBridge で検知した情報から引いてきます
- 退避先の S3 バケットは Lambda 環境変数に登録してあります
import boto3
import os
import json
ssm = boto3.client('ssm')
s3 = boto3.client('s3')
def handler(event, context):
print("Received event: " + json.dumps(event, indent=2))
instance_id = event['detail']['instance-id']
bucket_name = os.environ['BUCKET_NAME']
# SSM Run Command を使用してデータをバックアップ
response = ssm.send_command(
InstanceIds=[instance_id],
DocumentName='AWS-RunShellScript',
Parameters={
'commands': [
f'aws s3 sync /scratch s3://{bucket_name}/{instance_id}/'
]
}
)
command_id = response['Command']['CommandId']
print(f"Backup command sent. Command ID: {command_id}")
return {
'statusCode': 200,
'body': json.dumps('Spot instance interruption handled successfully!')
}
コンピューノードの IAM ロールに権限追加(対応必須)
コンピュートノードから退避用の S3 バケットへアクセスできる権限が必要です。以下の方法を参考に、ParallelCluster の設定を変更または追加してください。
設定する IAM ポリシーの例を以下に示します。S3 バケット名は、作成したバケットの実際の名前に置き換えてください。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:ListAllMyBuckets",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetBucketLocation"
],
"Resource": "arn:aws:s3:::rescue-spot-data-bucket"
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:PutObjectAcl",
"s3:GetObject",
"s3:GetObjectAcl",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::rescue-spot-data-bucket/*"
}
]
}
動作検証
インスタンスストア持ちのスポットインスタンスが起動可能なクラスター環境と、本記事で紹介したスポット中断検知の仕組みを CDK からデプロイした環境で動作確認します。
- 検証環境でのスポットインスタンス起動
- ダミーデータ作成
- AWS FIS からスポット中断リクエスト
- S3 へのデータ退避の確認
検証環境
項目 | 値 |
---|---|
CDK | 2.158.0 (build 4b8714d) |
AWS ParallelCluster | 3.10.1 |
OS | Ubuntu 22.04 |
Compute Node | c7gd.large |
Simultaneous Multi-Threading | 無効 |
以下のクラスターコンフィグから作成したクラスター環境を使用しました。
クラスターコンフィグ折りたたみ
Region: ap-northeast-1
Image:
Os: ubuntu2204
Tags:
- Key: Name
Value: rescue-spot-cluster
# ----------------------------------------------------------------
# Head Node Settings
# ----------------------------------------------------------------
HeadNode:
InstanceType: t4g.micro
Networking:
ElasticIp: false
SubnetId: subnet-035be95eeaa091603
LocalStorage:
RootVolume:
Size: 40
Encrypted: true
VolumeType: gp3
Iops: 3000
Throughput: 125
Iam:
AdditionalIamPolicies:
- Policy: arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
- Policy: arn:aws:iam::123456789012:policy/transfer-s3express
# ----------------------------------------------------------------
# Compute Node Settings
# ----------------------------------------------------------------
Scheduling:
Scheduler: slurm
SlurmSettings:
ScaledownIdletime: 5
SlurmQueues:
# ------ Compute 1 ------
- Name: p1
ComputeResources:
- Name: test
Instances:
- InstanceType: t4g.micro
MinCount: 0
MaxCount: 30
DisableSimultaneousMultithreading: true
ComputeSettings:
LocalStorage:
RootVolume:
Size: 40
Encrypted: true
VolumeType: gp3
Iops: 3000
Throughput: 125
CapacityType: SPOT
AllocationStrategy: price-capacity-optimized
Networking:
SubnetIds:
- subnet-035be95eeaa091603
- subnet-0ba7369f9caba6f93
PlacementGroup:
Enabled: false
Iam:
AdditionalIamPolicies:
- Policy: arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
- Policy: arn:aws:iam::123456789012:policy/transfer-s3express
# ------ Compute 2 ------
- Name: p2
ComputeResources:
- Name: c7glarge
Instances:
- InstanceType: c7g.large
MinCount: 0
MaxCount: 30
DisableSimultaneousMultithreading: true
ComputeSettings:
LocalStorage:
RootVolume:
Size: 40
Encrypted: true
VolumeType: gp3
Iops: 3000
Throughput: 125
CapacityType: SPOT
AllocationStrategy: price-capacity-optimized
Networking:
SubnetIds:
- subnet-035be95eeaa091603
- subnet-0ba7369f9caba6f93
PlacementGroup:
Enabled: false
Iam:
AdditionalIamPolicies:
- Policy: arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
- Policy: arn:aws:iam::123456789012:policy/transfer-s3express
# ------ Compute 2 ------
- Name: p3
ComputeResources:
- Name: c7gdlarge
Instances:
- InstanceType: c7gd.large
MinCount: 0
MaxCount: 30
DisableSimultaneousMultithreading: true
ComputeSettings:
LocalStorage:
RootVolume:
Size: 40
Encrypted: true
VolumeType: gp3
Iops: 3000
Throughput: 125
CapacityType: SPOT
AllocationStrategy: price-capacity-optimized
Networking:
SubnetIds:
- subnet-035be95eeaa091603
- subnet-0ba7369f9caba6f93
PlacementGroup:
Enabled: false
Iam:
AdditionalIamPolicies:
- Policy: arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
- Policy: arn:aws:iam::123456789012:policy/transfer-s3express
# ----------------------------------------------------------------
# Shared Storage Settings
# ----------------------------------------------------------------
# SharedStorage:
# - MountDir: /mnt/efs-elastic
# Name: efs1
# StorageType: Efs
# EfsSettings:
# FileSystemId: fs-0846dc947572a66a1
# - MountDir: /mnt/efs-bursting
# Name: efs2
# StorageType: Efs
# EfsSettings:
# FileSystemId: fs-046b02d3ba107c2b2
# ----------------------------------------------------------------
# Other Settings
# ----------------------------------------------------------------
Monitoring:
Logs:
CloudWatch:
Enabled: true
RetentionInDays: 180
DeletionPolicy: "Delete"
Dashboards:
CloudWatch:
Enabled: false
コンピュートノードを起動
テストジョブをサブミットして起動したコンピュートノードへ、AWS Systems Manager のセッションマネージャーを使用して接続しました。
インスタンスストアが/scratch
にマウントされていることを確認します。
ubuntu@p3-dy-c7gdlarge-1:~$ df -h | grep ephemeral
/dev/mapper/vg.01-lv_ephemeral 108G 24K 103G 1% /scratch
ダミーデータとして、100K のファイルを 1000 個作成しました。
$ cd /scratch
$ dd if=/dev/zero of=test.file bs=100K count=1;for i in {1..1000};do cp test.file test_${i}.file;done
スポットインスタンスの中断
AWS FIS を利用してコンピュートノードにスポット中断イベントを注入します。詳細は以下のブログを確認してください。
中断イベント注入後、コンピュートノードのメタデータから 05:14:58 に終了されることが確認できました。
$ TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"` && curl -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/spot/instance-action
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 56 100 56 0 0 62992 0 --:--:-- --:--:-- --:--:-- 56000
* Trying 169.254.169.254:80...
* Connected to 169.254.169.254 (169.254.169.254) port 80 (#0)
> GET /latest/meta-data/spot/instance-action HTTP/1.1
> Host: 169.254.169.254
> User-Agent: curl/7.81.0
> Accept: */*
> X-aws-ec2-metadata-token: AQAEANPE2BWOy3bN6eoXPwNMbJvXdHEjMTTAH_UGeVNZIN6B_urT4g==
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< X-Aws-Ec2-Metadata-Token-Ttl-Seconds: 21600
< Content-Type: text/plain
< Accept-Ranges: none
< Last-Modified: Fri, 13 Sep 2024 05:12:58 GMT
< Content-Length: 52
< Date: Fri, 13 Sep 2024 05:13:43 GMT
< Server: EC2ws
< Connection: close
<
* Closing connection 0
{"action":"terminate","time":"2024-09-13T05:14:58Z"}
S3 バケット確認
中断イベントを EventBridge で検知し Lambda が起動して、コンピュートノードに作成したダミーデータが S3 バケットへコピーされていました。
動作確認は終了です。
まとめ
本記事で紹介した方法により、スポットインスタンスの中断時でもローカルデータの退避が可能になります。AWS ParallelCluster を使用した HPC 環境において、長時間実行するジョブでスポットインスタンスとローカルストレージを併用している場合は、本構成の導入をご検討ください。
肝心なのは中間ファイルから処理を再実行できるかなので、AWS ParallelCluster の環境下で再実行できるのか、できないのか、できる場合は再実行方法も事前に検証しておくと良いでしょう。
おわりに
EventBridge から直接 SSM の Run Command を呼び出すことは可能ですが、中断通知が発行されたインスタンス ID を動的に指定できないという制限がありました。そのため、Lambda を挟む構成を採用しました。
この構成には以下の懸念事項があります。
- Lambda のコールドスタート
- 多くの場合は Lambda はコールドスタートとなり、貴重な 2 分間の中断通知時間のうち、わずかではありますが Lambda の起動待ちに時間を費やします。
- 大容量ファイルの退避
- 数 GB の大容量ファイルが大量に生成されるケースでは、2 分間で退避が完了しない可能性はあります
- Lambda のメモリサイズを上げることで処理速度が上がる可能性はありますので、中間ファイル生成のワークロードに応じた調整する余地があります。
- スポット中断の通知すべてに反応してしまう
- ParallelCluster のコンピュートノードの場合のみ Lambda を実行したかったのですが、判定に使える情報が見当たりませんでした。
大量のファイルへのアクセス速度を向上させる S3 Express の採用を検討しましたが、現時点で CDK の L2 Construct が対応していないことが分かりました。
今後の展望としては、CDK の S3 Express 対応されたら改めて設計を見直したいと思います。